package org.app.appjson.reader;

import org.app.appjson.exception.AppJsonException;
import org.app.appjson.reader.interfaces.CharReader;
import org.app.appjson.reader.interfaces.Lexer;

import java.io.IOException;
import java.util.Objects;

/**
 * Json词法分析器
 * 解析一串字符流，根据json的词法规则返回词法单元
 * @author lizhongsi
 * @version 1.0
 * @since 1.0
 */
public final class JsonLexer implements Lexer {

    private final CharReader reader;

    // nextToken：下一个词法单元，未被读取过
    // 定义为final，因为nextToken对象可复用
    // peek()返回nextToken
    // next()返回nextToken当前值，然后执行updateToken()更新nextToken，等待下次读取
    private final JsonToken nextToken;

    // 当前已被读取过的词法单元
    private final JsonToken token;
    private int index;

    public JsonLexer(CharReader in) throws AppJsonException {
        Objects.requireNonNull(in, "in is null.");
        this.reader = in;
        this.nextToken = new JsonToken();
        this.token = new JsonToken();
        // 更新一次nextToken，给peek()使用
        updateNextToken();
        this.index = -1;
    }

    @Override
    public boolean hasNext() {
        if (nextToken.getType() == JsonToken.END_DOCUMENT) {
            closeReader();
        }
        return nextToken.getType() != JsonToken.END_DOCUMENT;
    }

    /**
     * 读取一个词法单元（token）
     * @return JsonToken
     */
    @Override
    public JsonToken next() throws AppJsonException {
        // 用token保存nextToken，返回token
        token.setType(nextToken.getType());
        token.setValue(nextToken.getValue());
        if (hasNext()) {
            updateNextToken(); //更新nextToken
        }
        return token;
    }

    // 预览一个词法单元（读取nextToken的值），因为构造器中更新过一次，所以不会为null
    @Override
    public JsonToken peek() {
        return this.nextToken;
    }

    /**
     * 更新nextToken
     */
    private void updateNextToken() throws AppJsonException {
        try {
            nextToken.clear();
            char ch;
            for (;;) {
                if (!reader.hasNext()) {
                    nextToken.setType(JsonToken.END_DOCUMENT);
                    return ;
                }
                ch = reader.peek();
                // 跳过两个token之间的空字符，非空字符则跳出循环
                if (!isWhiteSpace(ch)) {
                    break;
                }
                reader.next();
            }
            switch (ch) {
                case '{':
                    nextToken.setType(JsonToken.BEGIN_OBJECT);
                    reader.next();
                    break;
                case '}':
                    nextToken.setType(JsonToken.END_OBJECT);
                    reader.next();
                    break;
                case '[':
                    nextToken.setType(JsonToken.BEGIN_ARRAY);
                    reader.next();
                    break;
                case ']':
                    nextToken.setType(JsonToken.END_ARRAY);
                    reader.next();
                    break;
                case ':':
                    nextToken.setType(JsonToken.SEP_COLON);
                    reader.next();
                    break;
                case ',':
                    nextToken.setType(JsonToken.SEP_COMMA);
                    reader.next();
                    break;
                case '\"':
                    nextToken.setType(JsonToken.STRING);
                    nextToken.setValue(reader.readString());
                    break;
                // null
                case 'n':
                    readExcept("null");
                    nextToken.setType(JsonToken.NULL);
                    break;
                // NULL
                case 'N':
                    readExcept("NULL");
                    nextToken.setType(JsonToken.NULL);
                    break;
                // true
                case 't':
                    readExcept("true");
                    nextToken.setValue("true");
                    nextToken.setType(JsonToken.BOOLEAN);
                    break;
                // TRUE
                case 'T':
                    readExcept("TRUE");
                    nextToken.setValue("true");
                    nextToken.setType(JsonToken.BOOLEAN);
                    break;
                // false
                case 'f':
                    readExcept("false");
                    nextToken.setValue("false");
                    nextToken.setType(JsonToken.BOOLEAN);
                    break;
                // FALSE
                case 'F':
                    readExcept("FALSE");
                    nextToken.setValue("false");
                    nextToken.setType(JsonToken.BOOLEAN);
                    break;
                case '-':
                    nextToken.setType(JsonToken.NUMBER);
                    nextToken.setValue(reader.readNumber());
                    break;
                default:
                    if (ch >= '0' && ch <= '9') {
                        nextToken.setType(JsonToken.NUMBER);
                        nextToken.setValue(reader.readNumber());
                        break;
                    } else {
                        throw new AppJsonException("JsonLexer error! index:" + reader.index() + ",char: " + ch);
                    }
            }
            index++;
        } catch (IOException e) {
            closeReader();
        }
    }

    @Override
    public int index() {
        return this.index;
    }

    public int readerIndex() {
        return this.reader.index();
    }

    /**
     * 判断一个字符是否是空白字符
     * @param ch 字符
     * @return 布尔值
     */
    private static boolean isWhiteSpace(char ch) {
        return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r';
    }

    // 读取一个期望值，如果不匹配则抛出异常
    private void readExcept(String expected) throws AppJsonException {
        try {
            for (int i = 0; i < expected.length(); i++) {
                char ch = reader.next();
                if (ch != expected.charAt(i)) {
                    throw new AppJsonException("JsonLexer error! Expect a \"" + expected + "\" but read char:" + ch + ",index:" + reader.index());
                }
            }
        }catch (IOException e) {
            closeReader();
        }
    }

    private void closeReader() {
        if (reader != null) {
            try {
                reader.close();
            } catch (Exception ignore) {
            }
        }
    }
}
